/*
 * Copyright 2004-2014 SmartBear Software
 *
 * Licensed under the EUPL, Version 1.1 or - as soon as they will be approved by the European Commission - subsequent
 * versions of the EUPL (the "Licence");
 * You may not use this work except in compliance with the Licence.
 * You may obtain a copy of the Licence at:
 *
 * http://ec.europa.eu/idabc/eupl
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the Licence is
 * distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
 * express or implied. See the Licence for the specific language governing permissions and limitations
 * under the Licence.
*/

package com.eviware.soapui.impl.wadl.inference.support;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.xmlbeans.XmlAnySimpleType;
import org.apache.xmlbeans.XmlBase64Binary;
import org.apache.xmlbeans.XmlBoolean;
import org.apache.xmlbeans.XmlDate;
import org.apache.xmlbeans.XmlDateTime;
import org.apache.xmlbeans.XmlDecimal;
import org.apache.xmlbeans.XmlGDay;
import org.apache.xmlbeans.XmlGMonth;
import org.apache.xmlbeans.XmlGYear;
import org.apache.xmlbeans.XmlGYearMonth;
import org.apache.xmlbeans.XmlHexBinary;
import org.apache.xmlbeans.XmlInteger;
import org.apache.xmlbeans.XmlNegativeInteger;
import org.apache.xmlbeans.XmlNonNegativeInteger;
import org.apache.xmlbeans.XmlNonPositiveInteger;
import org.apache.xmlbeans.XmlPositiveInteger;
import org.apache.xmlbeans.XmlString;
import org.apache.xmlbeans.XmlTime;

/**
 * XML Schema inferred from gathered XML data.
 *
 * @author Dain Nilsson
 */
public class TypeInferrer {
    private static TypeInferrer ref;
    private TypeTree types;
    private Map<XmlAnySimpleType, TypeTree> typeTable;

    /**
     * Get the instance of the XmlAnySimpleType with the type xs:<typeName>.
     *
     * @param typeName
     * @return Returns the XmlAnySimpleType, if available. Otherwise returns
     *         null.
     */
    public static XmlAnySimpleType getType(String typeName) {
        for (XmlAnySimpleType item : getRef().typeTable.keySet()) {
            if (item.schemaType().getName().getLocalPart().equals(typeName)) {
                return item;
            }
        }
        return null;
    }

    /**
     * Get the XmlAnySimpleType that describes a simple value that is empty.
     *
     * @return Returns the xs:string XmlAnySimpleType.
     */
    public static XmlAnySimpleType getBlankType() {
        return getRef().types.type;
    }

    /**
     * Given a value and a type, a new type will be returned that validates
     * values for both the given type, and the new value.
     *
     * @param value    The new value to expand the type for.
     * @param baseType The type to be expanded.
     * @return The new expanded type.
     */
    public static XmlAnySimpleType expandTypeForValue(String value, XmlAnySimpleType baseType) {
        return getRef().expandTypeForValueInternal(baseType, value);
    }

    /**
     * Given a simple value, infers the type of the value.
     *
     * @param value The value to assign a type to.
     * @return Returns the inferred type for the given value.
     */
    public static XmlAnySimpleType inferSimpleType(String value) {
        return getRef().inferSimpleTypeRec(value, getRef().types);
    }

    /**
     * Validates a string against an XmlAnySimpleType.
     *
     * @param value The value to validate.
     * @param type  The XmlAnySimpleType to validate against.
     * @return True if the value validates, false if not.
     */
    public static boolean validateSimpleType(String value, XmlAnySimpleType type) {
        try {
            type.setStringValue(value);
            return type.validate();
        } catch (Exception e) {
            return false;
        }
    }

    private static TypeInferrer getRef() {
        if (ref == null) {
            ref = new TypeInferrer();
        }
        return ref;
    }

    private TypeInferrer() {
        typeTable = new HashMap<XmlAnySimpleType, TypeTree>();
        TypeTree xmlbool = new TypeTree(XmlBoolean.Factory.newInstance());
        typeTable.put(xmlbool.type, xmlbool);
        TypeTree xmlbool2 = new TypeTree(XmlBoolean.Factory.newInstance());
        typeTable.put(xmlbool2.type, xmlbool2);
        TypeTree xmlnegint = new TypeTree(XmlNegativeInteger.Factory.newInstance());
        typeTable.put(xmlnegint.type, xmlnegint);
        TypeTree xmlposint = new TypeTree(XmlPositiveInteger.Factory.newInstance());
        typeTable.put(xmlposint.type, xmlposint);
        TypeTree xmlnonnegint = new TypeTree(XmlNonNegativeInteger.Factory.newInstance());
        typeTable.put(xmlnonnegint.type, xmlnonnegint);
        xmlnonnegint.addChild(xmlposint);
        xmlnonnegint.addChild(xmlbool);
        TypeTree xmlnonposint = new TypeTree(XmlNonPositiveInteger.Factory.newInstance());
        typeTable.put(xmlnonposint.type, xmlnonposint);
        xmlnonposint.addChild(xmlnegint);
        TypeTree xmlint = new TypeTree(XmlInteger.Factory.newInstance());
        typeTable.put(xmlint.type, xmlint);
        xmlint.addChild(xmlnonnegint);
        xmlint.addChild(xmlnonposint);
        TypeTree xmldec = new TypeTree(XmlDecimal.Factory.newInstance());
        typeTable.put(xmldec.type, xmldec);
        xmldec.addChild(xmlint);
        TypeTree xmldate = new TypeTree(XmlDate.Factory.newInstance());
        typeTable.put(xmldate.type, xmldate);
        TypeTree xmltime = new TypeTree(XmlTime.Factory.newInstance());
        typeTable.put(xmltime.type, xmltime);
        TypeTree xmldatetime = new TypeTree(XmlDateTime.Factory.newInstance());
        typeTable.put(xmldatetime.type, xmldatetime);
        TypeTree xmlhexbin = new TypeTree(XmlHexBinary.Factory.newInstance());
        typeTable.put(xmlhexbin.type, xmlhexbin);
        TypeTree xmlb64bin = new TypeTree(XmlBase64Binary.Factory.newInstance());
        typeTable.put(xmlb64bin.type, xmlb64bin);
        TypeTree xmlgyearmonth = new TypeTree(XmlGYearMonth.Factory.newInstance());
        typeTable.put(xmlgyearmonth.type, xmlgyearmonth);
        TypeTree xmlgyear = new TypeTree(XmlGYear.Factory.newInstance());
        typeTable.put(xmlgyear.type, xmlgyear);
        TypeTree xmlgmonth = new TypeTree(XmlGMonth.Factory.newInstance());
        typeTable.put(xmlgmonth.type, xmlgmonth);
        TypeTree xmlgday = new TypeTree(XmlGDay.Factory.newInstance());
        typeTable.put(xmlgday.type, xmlgday);
        TypeTree xmlstring = new TypeTree(XmlString.Factory.newInstance());
        typeTable.put(xmlstring.type, xmlstring);
        xmlstring.addChild(xmldec);
        xmlstring.addChild(xmldate);
        xmlstring.addChild(xmltime);
        xmlstring.addChild(xmldatetime);
        xmlstring.addChild(xmlbool2);
        xmlstring.addChild(xmlgyearmonth);
        xmlstring.addChild(xmlgyear);
        xmlstring.addChild(xmlgmonth);
        xmlstring.addChild(xmlgday);
        xmlstring.addChild(xmlhexbin);
        // xmlstring.addChild(xmlb64bin);
        types = xmlstring;
    }

    private XmlAnySimpleType expandTypeForValueInternal(XmlAnySimpleType type, String value) {
        TypeTree p = typeTable.get(type);
        while (!validateSimpleType(value, p.type)) {
            p = p.parent;
        }
        return p.type;
    }

    private XmlAnySimpleType inferSimpleTypeRec(String value, TypeTree p) {
        for (TypeTree item : p.children) {
            if (validateSimpleType(value, item.type)) {
                return inferSimpleTypeRec(value, item);
            }
        }
        return p.type;
    }

    private class TypeTree {
        public XmlAnySimpleType type;
        public TypeTree parent;
        public List<TypeTree> children;

        public TypeTree(XmlAnySimpleType type) {
            this.type = type;
            children = new ArrayList<TypeTree>();
        }

        public void addChild(TypeTree type) {
            children.add(type);
            type.parent = this;
        }
    }

}
